﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Game15
{
    class GameFifteen
    {
        private const string EMPTYCELLVALUE = " ";
        private const int MATRIXSIZEROWS = 4;
        private const int MATRIXSIZECOLUMNS = 4;
        private const int TOPSCORESAMOUNT = 5;
        private const string TOPSCORESFILENAME = "../../Top.txt";
        private const string TOPSCORESPERSONPATTERN = @"^\d+\. (.+) --> (\d+) moves?$";
        private static readonly int[] directionRow = { -1, 0, 1, 0 };
        private static readonly int[] directionColumn = { 0, 1, 0, -1 };
        private static int emptyCellRow;
        private static int emptyCellColumn;
        private static string[,] matrix;
        private static int turn;

        public static string[,] Matrix
        {
            get
            {
                return GameFifteen.matrix;
            }
            set
            {
                GameFifteen.matrix = value;
            }
        }

        public static int EmptyCellRow
        {
            get
            {
                return GameFifteen.emptyCellRow;
            }
            set
            {
                GameFifteen.emptyCellRow = value;
            }
        }

        public static int EmptyCellColumn
        {
            get
            {
                return GameFifteen.emptyCellColumn;
            }
            set
            {
                GameFifteen.emptyCellColumn = value;
            }
        }

        private static bool CellNumberToDirection(int cellNumber, out int direction)
        {
            for (int dir = 0; dir < directionRow.Length; dir++)
            {
                bool isDirValid = IsCellInsideMatrix(dir);

                if (isDirValid)
                {
                    int nextCellRow = emptyCellRow + directionRow[dir];
                    int nextCellColumn = emptyCellColumn + directionColumn[dir];

                    if (matrix[nextCellRow,nextCellColumn] == cellNumber.ToString())
                    {
                        direction = dir;
                        return true;
                    }
                }
            }
            direction = -1;
            return false;
        }

        private static void TheEnd()
        {
            string moves;
            if (turn == 1)
            {
                moves = "1 move";
            }
            else
            {
                moves = string.Format("{0} moves", turn);
            }
            Console.WriteLine("Congratulations! You won the game in {0}.", moves);

            string[] topScores = GetTopScoresFromFile();
            if (topScores[TOPSCORESAMOUNT - 1] != null)
            {
                string lowestScore = Regex.Replace(topScores[TOPSCORESAMOUNT - 1], TOPSCORESPERSONPATTERN, @"$2");
                if (int.Parse(lowestScore) < turn)
                {
                    Console.WriteLine("You couldn't get in the top {0} scoreboard.", TOPSCORESAMOUNT);
                    return;
                }
            }
            UpgradeTopScore();
        }

        private static string[] GetTopScoresFromFile()
        {
            try
            {
                string[] topScores = new string[TOPSCORESAMOUNT + 1];
                StreamReader topReader = new StreamReader(TOPSCORESFILENAME);
                using (topReader)
                {
                    int line = 0;
                    while (!topReader.EndOfStream && line < TOPSCORESAMOUNT)
                    {
                        topScores[line] = topReader.ReadLine();
                        line++;
                    }
                }
                return topScores;
            }
            catch (FileNotFoundException)
            {
                StreamWriter topWriter = new StreamWriter(TOPSCORESFILENAME);
                using (topWriter)
                {
                    topWriter.Write("");
                }
                return new string[TOPSCORESAMOUNT];
            }
        }

        private static void InitializeMatrix()
        {
            matrix = new string[MATRIXSIZEROWS,MATRIXSIZECOLUMNS];
            int cellValue = 1;

            for (int row = 0; row < MATRIXSIZEROWS; row++)
            {
                for (int column = 0; column < MATRIXSIZECOLUMNS; column++)
                {
                    matrix[row,column] = cellValue.ToString();

                    cellValue++;
                }
            }

            emptyCellRow = MATRIXSIZEROWS - 1;
            emptyCellColumn = MATRIXSIZECOLUMNS - 1;
            matrix[emptyCellRow,emptyCellColumn] = EMPTYCELLVALUE;
        }

        private static bool IsCellInsideMatrix(int direction)
        {
            int nextCellRow = emptyCellRow + directionRow[direction];
            bool isRowValid = (nextCellRow >= 0 && nextCellRow < MATRIXSIZEROWS);

            int nextCellColumn = emptyCellColumn + directionColumn[direction];
            bool isColumnValid = (nextCellColumn >= 0 && nextCellColumn < MATRIXSIZECOLUMNS);

            bool isCellValid = isRowValid && isColumnValid;
            return isCellValid;
        }

        private static bool CheckIfGameIsWon()
        {
            bool isEmptyCellInPlace = (emptyCellRow == MATRIXSIZEROWS - 1) && (emptyCellColumn == MATRIXSIZECOLUMNS - 1);

            if (!isEmptyCellInPlace)
            {
                return false;
            }

            int cellValue = 1;
            int matrixSize = MATRIXSIZEROWS * MATRIXSIZECOLUMNS;

            for (int row = 0; row < MATRIXSIZEROWS; row++)
            {
                for (int column = 0; column < MATRIXSIZECOLUMNS && cellValue < matrixSize; column++)
                {
                    if (matrix[row,column] != cellValue.ToString())
                    {
                        return false;
                    }

                    cellValue++;
                }
            }

            return true;
        }

        private static void MoveCell(int direction)
        {
            int nextCellRow = emptyCellRow + directionRow[direction];
            int nextCellColumn = emptyCellColumn + directionColumn[direction];
            matrix[emptyCellRow,emptyCellColumn] = matrix[nextCellRow,nextCellColumn];
            matrix[nextCellRow,nextCellColumn] = EMPTYCELLVALUE;
            emptyCellRow = nextCellRow;
            emptyCellColumn = nextCellColumn;
            turn++;
        }

        private static void NextMove(int cellNumber)
        {
            int matrixSize = MATRIXSIZEROWS * MATRIXSIZECOLUMNS;
            if (cellNumber <= 0 || cellNumber >= matrixSize)
            {
                Console.WriteLine("That cell does not exist in the matrix.");
                return;
            }

            int direction; 
            bool moveIsValid = CellNumberToDirection(cellNumber, out direction);
            if (!moveIsValid)
            {
                Console.WriteLine("Illegal move!");
                return;
            }

            MoveCell(direction);
            PrintMatrix();
        }

        public static void PlayGame()
        {
            while (true)
            {
                InitializeMatrix();
                ShuffleMatrix();
                turn = 0;
                PrintWelcomeMessage();
                PrintMatrix();
                while (true)
                {
                    Console.Write("Enter a number to move: ");
                    string consoleInputLine = Console.ReadLine();
                    int cellNumber;
                    if (int.TryParse(consoleInputLine, out cellNumber))
                    {
                        //Input is a cell number.
                        NextMove(cellNumber);
                        if (CheckIfGameIsWon())
                        {
                            TheEnd();
                            break;
                        }
                    }
                    else
                    {
                        //Input is a command.
                        if (consoleInputLine == "restart")
                        {
                            break;
                        }
                        switch (consoleInputLine)
                        {
                            case "top":
                                PrintTopScores();
                                break;
                            case "exit":
                                Console.WriteLine("Good bye!");
                                return;
                            default:
                                Console.WriteLine("Illegal command!");
                                break;
                        }
                    }
                }
            }
        }

        private static void PrintMatrix()
        {
            StringBuilder horizontalBorder = new StringBuilder("  ");
            for (int i = 0; i < MATRIXSIZECOLUMNS; i++)
            {
                horizontalBorder.Append("---");
            }
            horizontalBorder.Append("- ");
            Console.WriteLine(horizontalBorder);
            for (int row = 0; row < MATRIXSIZEROWS; row++)
            {
                Console.Write(" |");
                for (int column = 0; column < MATRIXSIZECOLUMNS; column++)
                {
                    Console.Write("{0,3}", matrix[row,column]);
                }
                Console.WriteLine(" |");
            }
            Console.WriteLine(horizontalBorder);
        }

        private static void PrintTopScores()
        {
            Console.WriteLine("Scoreboard:");
            string[] topScores = GetTopScoresFromFile();
            if (topScores[0] == null)
            {
                Console.WriteLine("There are no scores to display yet.");
            }
            else
            {
                foreach (string score in topScores)
                {
                    if (score != null)
                    {
                        Console.WriteLine(score);
                    }
                }
            }
        }

        private static void PrintWelcomeMessage()
        {
            Console.Write("Welcome to the game \"15\". ");
            Console.WriteLine("Please try to arrange the numbers sequentially. ");
            Console.WriteLine("Use 'top' to view the top scoreboard, " +
                              "'restart' to start a new game and 'exit'  to quit the game.");
        }

        private static void ShuffleMatrix()
        {
            Random random = new Random();
            int matrixSize = MATRIXSIZEROWS * MATRIXSIZECOLUMNS;
            int shuffles = random.Next(matrixSize, matrixSize * 100);
            for (int i = 0; i < shuffles; i++)
            {
                int direction = random.Next(directionRow.Length);
                if (IsCellInsideMatrix(direction))
                {
                    MoveCell(direction);
                }
            }
            if (CheckIfGameIsWon())
            {
                ShuffleMatrix();
            }
        }

        private static void UpgradeTopScore()
        {
            string[] topScores = GetTopScoresFromFile();
            Console.Write("Please enter your name for the top scoreboard: ");
            string name = Console.ReadLine();
            if (name == string.Empty)
            {
                name = "Anonymous";
            }

            topScores[TOPSCORESAMOUNT] = string.Format("0. {0} --> {1} move", name, turn);
            Array.Sort(topScores);
            NameAndScore[] topScoresPairs = UpgradeTopScorePairs(topScores);
            IOrderedEnumerable<NameAndScore> sortedScores =
            topScoresPairs.OrderBy(x => x.Score).ThenBy(x => x.Name);
            UpgradeTopScoreInFile(sortedScores);
        }

        private static void UpgradeTopScoreInFile(IOrderedEnumerable<NameAndScore> sortedScores)
        {
            StreamWriter topWriter = new StreamWriter(TOPSCORESFILENAME);
            using (topWriter)
            {
                int position = 1;
                foreach (NameAndScore pair in sortedScores)
                {
                    string name = pair.Name;
                    int score = pair.Score;
                    string toWrite = string.Format(
                        "{0}. {1} --> {2} move", position, name, score);
                    if (score > 1)
                    {
                        toWrite += "s";
                    }
                    topWriter.WriteLine(toWrite);
                    position++;
                }
            }
        }

        private static NameAndScore[] UpgradeTopScorePairs(string[] topScores)
        {
            int startIndex = 0;
            while (topScores[startIndex] == null)
            {
                startIndex++;
            }

            int arraySize = Math.Min(TOPSCORESAMOUNT - startIndex + 1, TOPSCORESAMOUNT);
            NameAndScore[] topScoresPairs = new NameAndScore[arraySize];
            for (int topScoresPairsIndex = 0; topScoresPairsIndex < arraySize; topScoresPairsIndex++)
            {
                int topScoresIndex = topScoresPairsIndex + startIndex;
                string name = Regex.Replace(topScores[topScoresIndex], TOPSCORESPERSONPATTERN, @"$1");
                string score = Regex.Replace(topScores[topScoresIndex], TOPSCORESPERSONPATTERN, @"$2");
                int scoreInt = int.Parse(score);
                topScoresPairs[topScoresPairsIndex] = new NameAndScore(name, scoreInt);
            }

            return topScoresPairs;
        }

        static void Main()
        {
            PlayGame();
        }
    }
}